/blog/
Creating standardized config files in Rust
So one of the important things to writing good secure code is handling sensitive information in your application. I’m doing this right now for the CMS I’m building to manage this website, so I thought I’d share how I’m doing it. But first I’d like to be clear what I mean by sensitive information:
Sensitive Application Information: Information providing any level of detail about a person, place or thing. Where a "person" is any person. A "place" is any locative data. A "thing" is a catch all but really focused on any system related data, network data, or secrets.
You might notice my definition is a bit more stringent than you might have normally heard. This is kind of a derivative of the principle of least privilege, you don’t really need to ever add anything about any person, place or thing from outside your code inside your code, so don’t. A lot of people tend to think just keeping secrets out of code is enough, I would say those people are wrong. You often don’t know how something is going to be used against you until it is too late, so just try not to give anyone anything to use against you.
So to get the necessary sensitive information out of my code I need to put it somewhere else. For now that’s going to be a config file in a standard shared location. In the future maybe I’ll pull from environment variables or direct inject from a secrets manager like Hashicorp Vault. Anywhere outside the code base is better than inside the codebase. After that, there are many, many more levels of defense you can use to hide your sensitive information.
So here is a pretty basic JSON file loader function. It uses the dirs crate to identify the system user configuration directory and serde_json to parse a JSON file into the very basic config struct I have already created for the CMS app.
pub fn load_config() -> SiteConfig {
let mut site_config = String::new();
// Build a PathBuf from the system user config dir as the base
let mut config_file_path: PathBuf = match dirs::config_dir() {
Some(val) => PathBuf::from(val),
_ => panic!("No system config value found!"),
};
config_file_path.push("cms");
config_file_path.push("default");
// File read
let mut _file = match fs::File::open(&config_file_path) {
Err(why) => panic!("Couldn’t open config file: {}", why),
Ok(mut _file) => _file.read_to_string(&mut site_config),
};
// Deserialize the JSON
let deserialized_site_config: SiteConfig = match serde_json::from_str(&site_config) {
Err(why) => panic!("Config couldn’t be deserialized: {}", why),
Ok(value) => value,
};
deserialized_site_config
}
The mutable string is necessary for the read_to_string()
call after we open the file. The dirs::config_dir()
call returns the mostly cross platform XDG default configuration storage directory or fails flat out. We open the file with the standard filesystem tools, fs::File::open()
and read that into our mut string with _file.read_to_string()
. Then we deserialize into our previously created config struct with serde_json::from_str()
.
I used a lot of panics here as this is a pretty critical part of the application and failing if we don’t have the data is preferable to operating on bad defaults or some such.